package dse.messages.singleversion;

import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;

import dse.*;
import dse.messages.*;
import dse.messages.multiversion.*;

public class SingleVersionTransaction extends Transaction implements
		ShareDownloader, VersionReader {
	
	private DataObject desiredObject;
	private LinkedList<Node> residentNodes;
	private double begin;
	private double end;
	private int validShareCount;
	
	private int sharesToBeUpdate;
	private Version version;
	
	private TreeMap<Version, LinkedList<Node>> sharesDownloaded;
	private int successCount;
	private int failureCount;
	private boolean failed;
	
	public SingleVersionTransaction(Experiment experiment,
			int id,
			Node requester,
			DataObject desiredObject,
			double time){
		super(id, requester, experiment, time);
		this.desiredObject = desiredObject;
		this.sharesToBeUpdate = desiredObject.shareCount();
		if(isUpdate && isPartialUpdate)
		{
			this.sharesToBeUpdate = (int)(desiredObject.shareCount()*Math.random());
			//System.err.println("Partial Update:" + sharesToBeUpdate);
		}		
	}

	public void start (ExperimentRecorder recorder)
	{			
		this.residentNodes = MessageUtil.getNodesByDht(experiment.network(), requester, desiredObject, experiment.modules().assigner());
		if(isUpdate)
		{
			recorder.dataUpdateInitiated(
					super.id, requester, desiredObject, this.startTime
					);
			//recorder.debug(this, "start", time);
			this.version = new Version(requester.sequence(), requester.id());
			this.validShareCount = desiredObject.shareCount() - 
				experiment.modules().nodeEventScheduler().getEventsPerPeriod();
			for(int i=0; i<this.sharesToBeUpdate; i++)
				sendUpdateShareRequest(recorder, residentNodes.get(i), this.startTime, i);
		}
		else
		{
			recorder.dataRequestInitiated(
					super.id, requester, desiredObject, this.startTime
					);
			//recorder.debug(this, "start", time);
			this.validShareCount = desiredObject.requiredShareCount();
			for(int i=0; (i<this.validShareCount)&&(i<residentNodes.size()); i++)
			{
				sendGetLatestShareRequest(recorder, residentNodes.get(i), startTime);
			}
		}
	}
	
	public void shareDownloaded (
			ExperimentRecorder recorder,
			Node from,
			DataObject dobj, 
			int shareIndex,
			double timeLeft
			)
	{;}
	
	public void shareUpdated (
			ExperimentRecorder recorder,
			Node to,
			DataObject dobj, 
			int shareIndex,
			double timeLeft
			)
	{
		
		
		// This should really never fail...
		assert (dobj == desiredObject);
		
		if (failed)
			return;
		
		if (finished) {
			recorder.warn(
					"SingleVersionTransaction #" + super.id + ": " +
					" Share #" + shareIndex + " was updated AFTER the " +
					"entire data object was already successfully " +
					"updated"
					);
			return;
		}
		
		this.successCount++;
		if((successCount+failureCount) >= this.sharesToBeUpdate)
		{
			if(this.successCount >= validShareCount)
				transactionSucceed(recorder, timeLeft);
			else
				transactionFailed(recorder, timeLeft);
		}
	}
	
	public void shareDownloadFailed (
			ExperimentRecorder recorder, 
			DataObject dobj, 
			int shareIndex,
			double timeLeft
			)
	{;}
	
	public void shareUpdateFailed (
			ExperimentRecorder recorder, 
			DataObject dobj, 
			int shareIndex,
			double timeLeft
			)
	{
		//recorder.debug(
				//this, 
				//"shareUpdateFailed{dobj " + dobj.id() + " shr" + 
				//shareIndex + "} by " + requester + " in trans#" + super.id,
				//timeLeft
				//);
		
		// Have we already declared failure?
		if (failed)
			return;
		
		if (requester.isUp() == false)
			// Then we are pretty much dead
			transactionFailed(recorder, timeLeft);
		else {
			this.failureCount++;
			if((successCount + failureCount) >= this.sharesToBeUpdate)
			{
				if(this.successCount >= validShareCount)
					transactionSucceed(recorder, timeLeft);
				else
					transactionFailed(recorder, timeLeft);
			}
		}
	}

	public boolean isDoneDownloadingShares () {
		return (failed || finished);
	}
	
	public boolean isDoneUpdatingShares () {
		return (failed || finished);
	}
	
	private void sendUpdateShareRequest(ExperimentRecorder recorder, Node node, Double time, int i)
	{
		if (node == null) {
			// We should turn this into a warning ...
			recorder.warn(
					"Share assigner failed to map share to node in cluster!"
					);
			return;
		}
		
		Message m = super.createShareUpdateV6Message(requester, node, desiredObject, 
				version, MessagePurpose.DataObjectUpdate, this);
		recorder.debug(this, "new msg: " + m, time);
		experiment.messageQueue().enqueueMessage(m, time);
		if(i == 0)
			begin = m.endTime();
		else if(i == this.sharesToBeUpdate-1){
			end = m.endTime();
			recorder.updateDuration(end - begin);
		}
	}
	
	private void sendGetLatestShareRequest(ExperimentRecorder recorder, Node node, double time)
	{
		if (node == null) {
			// We should turn this into a warning ...
			recorder.warn(
					"Share assigner failed to map share to node in cluster!"
					);
			return;
		}
		
		Message m = super.createRequestLatestShareV2Message(requester, node, desiredObject, 
				MessagePurpose.DataObjectRequest, this);
		recorder.debug(this, "new msg: " + m, time);
		experiment.messageQueue().enqueueMessage(m, time);
	}
	
	@Override
	public void readVersion(ExperimentRecorder recorder, TreeSet<Version> versions, double timeLeft, Node from)
	{;}
	
	public void readVersion(ExperimentRecorder recorder, Node from, Version version, double timeLeft)
	{
		if(failed)
			return;
		if(version == null)
		{
			failureCount++;
			if(successCount+failureCount>= this.validShareCount)
				processVersion(recorder, timeLeft);
			return;
		}
		if(sharesDownloaded == null)
			sharesDownloaded = new TreeMap<Version, LinkedList<Node>>();
		//if(successCount >= validShareCount)
			//return;
		successCount++;
		if(this.sharesDownloaded.get(version) == null)
		{
			this.sharesDownloaded.put(version, new LinkedList<Node>());
			this.sharesDownloaded.get(version).add(from);
		}
		else
			this.sharesDownloaded.get(version).add(from);
		
		if(successCount + failureCount >= this.validShareCount)
			processVersion(recorder, timeLeft);
	}
	
	public void readVersion(ExperimentRecorder recorder, int seq, double timeLeft){}
	public void readVersion(ExperimentRecorder recorder, int seq, int index, double time){}
	public void readVersion(ExperimentRecorder recorder, double time, LinkedList<Version> versions, Version version, Node from){}

	private void processVersion(ExperimentRecorder recorder, double timeLeft)
	{
		if((sharesDownloaded==null) || (sharesDownloaded.size()==0))
		{
			transactionFailed(recorder, timeLeft);
			return;
		}
		
		Map.Entry<Version, LinkedList<Node>> vn = sharesDownloaded.pollLastEntry();
		while((vn.getValue().size()<this.desiredObject.requiredShareCount()) && (sharesDownloaded.size()>0))
		{
			vn = sharesDownloaded.pollLastEntry();
		}
		
		if(vn.getValue().size()>=this.desiredObject.requiredShareCount())
		{
			transactionSucceed(recorder, timeLeft);	
		}
		else
		{				
			transactionFailed(recorder, timeLeft);
		}
	}
	
	private void transactionSucceed(ExperimentRecorder recorder, double timeLeft)
	{
		finished = true;
		failed  = false;
		//System.out.println("Data ID : " + desiredObject.id() + "Version : " + maxVersion.getSequence() + " , " + maxVersion.getClientID());
		if(isUpdate)
		{
			recorder.debug(
					"GetDhtDataSharesWithVersionTransaction trans#" + super.id + 
					" is finshed!!!"
					);
			recorder.dataUpdateFinished(super.id, requester, desiredObject, true, timeLeft);
		}
		else
		{
			recorder.debug(
					"GetDhtDataSharesWithVersionTransaction trans#" + super.id + 
					" is finshed!!!"
					);
			recorder.dataRequestFinished(super.id, requester, desiredObject, true, this.residentNodes.get(this.validShareCount-1),
					false, timeLeft);
		}
	}
	
	private void transactionFailed (ExperimentRecorder recorder, double timeLeft)
	{
		recorder.debug(this, "transFailed", timeLeft);
		
		finished = true;
		failed = true;
		if(isUpdate)
			recorder.dataUpdateFinished(super.id, requester, desiredObject, false, timeLeft);
		else
			recorder.dataRequestFinished(super.id, requester, desiredObject, false, null, false, timeLeft);
	}
	public Transaction getTransaction(){
		return this;
	}
}
